// --------------------------------------------------------------------------------------------------------------------- 
// <copyright file="Updater.cs" company="Scrum for Team System">
//   None
// </copyright>
// <summary>
//   Defines the Updater type.
// </summary>
// ---------------------------------------------------------------------------------------------------------------------
namespace ScrumForTeamSystem.EventService.TransitionService
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;

    using DataStructures;

    using Infrastructure;

    using Microsoft.TeamFoundation.WorkItemTracking.Client;

    using Properties;

    /// <summary>
    /// Defines the Updater type.
    /// </summary>
    internal class Updater
    {
        /// <summary>
        /// Gets or sets the process message.
        /// </summary>
        /// <value>The process message.</value>
        public Action<string> ProcessMessage
        {
            get;
            set;
        }

        /// <summary>
        /// Synchronises the [targetWorkItem] using the specified rules and infers the correct state.
        /// </summary>
        /// <param name="targetWorkItem">The target work item.</param>
        /// <param name="rules">The rules.</param>
        public void Synchronise(WorkItem targetWorkItem, IEnumerable<TransitionRule> rules)
        {
            if (targetWorkItem == null)
            {
                throw new ArgumentNullException("targetWorkItem");
            }

            foreach (var rule in rules)
            {
                var transitionRule = rule;

                Func<WorkItem, bool> isValidSource =
                            w => w.Fields.OfType<Field>()
                                .Any(f => f.ReferenceName.Equals(transitionRule.Transition.Field) 
                                    && (transitionRule.Transition.To.Equals(Transition.Wildcard) || f.Value.Equals(transitionRule.Transition.To)));

                // Get the first source item that triggers this rule
                var sourceWorkItem = rule.LinkType.Target == LinkOption.Self
                    ? targetWorkItem
                    : targetWorkItem.GetLinkedItems(
                                transitionRule.LinkType.Name, transitionRule.WorkItemTypeName.Source).Where(isValidSource)
                                .FirstOrDefault();

                if (sourceWorkItem == null)
                {
                    continue;
                }

                this.ApplyRule(targetWorkItem, sourceWorkItem, transitionRule);

                if (targetWorkItem.IsDirty)
                {
                    GlobalHelpers.ApplyTimestamp(targetWorkItem, Settings.Default.TimestampField).Save();
                }
            }
        }

        /// <summary>
        /// Executes the specified rules against the [targetWorkItem].
        /// </summary>
        /// <param name="targetWorkItem">The target work item.</param>
        /// <param name="sourceWorkItem">The source work item.</param>
        /// <param name="rules">The rules.</param>
        public void Execute(WorkItem targetWorkItem, WorkItem sourceWorkItem, IEnumerable<TransitionRule> rules)
        {
            if (targetWorkItem == null)
            {
                throw new ArgumentNullException("targetWorkItem");
            }

            if (sourceWorkItem == null)
            {
                throw new ArgumentNullException("sourceWorkItem");
            }

            foreach (var rule in rules)
            {
                this.ApplyRule(targetWorkItem, sourceWorkItem, rule);
            }

            if (targetWorkItem.IsDirty)
            {
                targetWorkItem = GlobalHelpers.ValidateWorkItem(
                    GlobalHelpers.ApplyTimestamp(
                        targetWorkItem, Settings.Default.TimestampField));

                targetWorkItem.Save();
            }
        }

        /// <summary>
        /// Determines whether [the specified target work item] [is eligable state].
        /// </summary>
        /// <param name="targetWorkItem">The target work item.</param>
        /// <param name="transitionRule">The transition rule.</param>
        /// <returns>
        /// <c>true</c> if [the specified target work item] [is eligable state]; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsEligableState(WorkItem targetWorkItem, TransitionRule transitionRule)
        {
            return transitionRule.EligibleTargetStates.Contains(Transition.Wildcard)
                   || transitionRule.EligibleTargetStates.Contains(targetWorkItem.State);
        }

        /// <summary>
        /// Gets a flag to indicate if the sibling conditions have been met.
        /// </summary>
        /// <param name="targetWorkItem">The target work item.</param>
        /// <param name="rule">The transition rule.</param>
        /// <returns>An indication of if the meet sibling conditions have been met.</returns>
        private static bool HasMetAllSiblingConditions(WorkItem targetWorkItem, TransitionRule rule)
        {
            var hasMetAllConditions = true;

            foreach (var siblingCondition in rule.SiblingConditions)
            {
                var siblings =
                        targetWorkItem.GetLinkedItems(
                                siblingCondition.LinkTypeName,
                                siblingCondition.SiblingTypeName,
                                siblingCondition.ExcludedStates).ToArray();

                if (siblings.Count() == 0)
                {
                    continue;
                }

                var condition = siblingCondition;

                Func<WorkItem, bool> isMatch =
                        w =>
                        w.Fields.Cast<Field>().Any(f => f.ReferenceName.Equals(condition.FieldName))
                        && Equals(w.Fields[condition.FieldName].Value, condition.ComparisonValue);

                hasMetAllConditions = siblingCondition.ComparisonType == SiblingComparisonType.All
                                              ? siblings.All(isMatch)
                                              : siblings.Any(isMatch);

                if (!hasMetAllConditions)
                {
                    break;
                }
            }

            return hasMetAllConditions;
        }

        /// <summary>
        /// Gets the new value.
        /// </summary>
        /// <param name="replacement">The transition rule.</param>
        /// <param name="sourceWorkItem">The source work item.</param>
        /// <returns>The replacment value.</returns>
        private static object GetNewValue(Replacement replacement, WorkItem sourceWorkItem)
        {
            object newValue;

            switch (replacement.ReplacementOption)
            {
                case ReplacementValueOption.Specified:
                    newValue = replacement.Value;
                    break;
                case ReplacementValueOption.Expression:
                    var expression = ExpressionCalculator.Instance.GetExpressionMethod(replacement.Value, replacement.DecimalPlaces);
                    newValue = expression(sourceWorkItem);
                    break;
                default:
                    throw new ArgumentOutOfRangeException("replacement");
            }

            return newValue;
        }

        /// <summary>
        /// Applies the rule.
        /// </summary>
        /// <param name="targetWorkItem">The target work item.</param>
        /// <param name="sourceWorkItem">The source work item.</param>
        /// <param name="rule">The transition rule.</param>
        private void ApplyRule(WorkItem targetWorkItem, WorkItem sourceWorkItem, TransitionRule rule)
        {
            if (!IsEligableState(targetWorkItem, rule))
            {
                return;
            }

            if (!HasMetAllSiblingConditions(targetWorkItem, rule))
            {
                return;
            }

            foreach (var replacement in rule.Replacements)
            {
                var newValue = GetNewValue(replacement, sourceWorkItem);
                var currentValue = targetWorkItem.Fields[replacement.TargetField].Value;

                if (!Equals(currentValue, newValue))
                {
                    if (!targetWorkItem.IsOpen)
                    {
                        targetWorkItem.Open();
                    }

                    targetWorkItem.Fields[replacement.TargetField].Value = newValue;

                    if (!string.IsNullOrEmpty(rule.ChangeNote))
                    {
                        if (string.IsNullOrEmpty(targetWorkItem.History))
                        {
                            targetWorkItem.History = Settings.Default.ServiceName;
                        }

                        targetWorkItem.History = string.Concat(targetWorkItem.History, " - ", rule.ChangeNote);
                    }

                    this.RaiseProcessMessage(
                            string.Format(
                                    CultureInfo.InvariantCulture,
                                    Resources.String011,
                                    targetWorkItem.Id,
                                    replacement.TargetField,
                                    currentValue,
                                    newValue));
                }
            }
        }

        /// <summary>
        /// Raises the process message.
        /// </summary>
        /// <param name="message">The message.</param>
        private void RaiseProcessMessage(string message)
        {
            if (this.ProcessMessage == null)
            {
                return;
            }

            this.ProcessMessage(message);
        }
    }
}